Skip to content

Server implementation of Streamable HTTP transport #266

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
Apr 7, 2025

Conversation

ihrpr
Copy link
Contributor

@ihrpr ihrpr commented Apr 5, 2025

Introduces support for the new Streamable HTTP transport on the server side.

Builds on the work of @gylove1994 in #230.

To do

  • Remove distinction around local sessions

Follow ups

  • Resumability
  • Session management
  • Client implementation
  • server and client to handle requests returning not only streams but also JSON

@ihrpr ihrpr marked this pull request as ready for review April 7, 2025 13:33
Copy link
Member

@jspahrsummers jspahrsummers left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WIP review—will pick it up again later—but here are some preliminary comments that may require changes.

Comment on lines 13 to 18
/**
* The session ID SHOULD be globally unique and cryptographically secure (e.g., a securely generated UUID, a JWT, or a cryptographic hash)
*
* When sessionId is not set, the transport will be in stateless mode.
*/
sessionId: string | undefined;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops—I know I suggested this, but I think we actually need to do something different. It'd probably be better for this to accept a function (or undefined) which generates a session ID.

Then, the transport can call the function if it receives an InitializeRequest, but won't call it to generate a session ID otherwise. This seems valuable to avoid wasteful computation, but also to more clearly indicate the semantics here.

Copy link
Member

@jspahrsummers jspahrsummers left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Finalizing review comments now 🙏 I'm skipping reviewing the tests for the moment, as changes to the interface/implementation would probably invalidate a review there.

@ihrpr ihrpr requested a review from jspahrsummers April 7, 2025 16:31
Copy link
Member

@jspahrsummers jspahrsummers left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, looks good!

coolkid

Comment on lines +6 to +37
// Mock IncomingMessage
function createMockRequest(options: {
method: string;
headers: Record<string, string | string[] | undefined>;
body?: string;
}): IncomingMessage {
const readable = new Readable();
readable._read = () => { };
if (options.body) {
readable.push(options.body);
readable.push(null);
}

return Object.assign(readable, {
method: options.method,
headers: options.headers,
}) as IncomingMessage;
}

// Mock ServerResponse
function createMockResponse(): jest.Mocked<ServerResponse> {
const response = {
writeHead: jest.fn().mockReturnThis(),
write: jest.fn().mockReturnThis(),
end: jest.fn().mockReturnThis(),
on: jest.fn().mockReturnThis(),
emit: jest.fn().mockReturnThis(),
getHeader: jest.fn(),
setHeader: jest.fn(),
} as unknown as jest.Mocked<ServerResponse>;
return response;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude says the tests are good, so I'll defer to Claude 😂

However… we may want to consider not mocking these classes. Feels fragile and error-prone. It might be better to actually just spin up an HTTP server in the tests. Anyway, this isn't blocking—can do it in a follow-up if we want.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll do in the follow up if it's okay, planning to add more comprehensive tests with client implementation and the complete e2e flow

@beaulac
Copy link
Contributor

beaulac commented Apr 7, 2025

Looks good, just want to check regarding the non-SSE case (batched JSON response) is not supported or mentioned in the follow-ups - would it be implemented in this class later or as a separate transport?

@ihrpr
Copy link
Contributor Author

ihrpr commented Apr 7, 2025

Looks good, just want to check regarding the non-SSE case (batched JSON response) is not supported or mentioned in the follow-ups - would it be implemented in this class later or as a separate transport?

@beaulac good point, yes, it's planned, will add it to the follow-ups

@ihrpr ihrpr merged commit 693fdc3 into main Apr 7, 2025
4 checks passed
@ihrpr ihrpr deleted the ihrpr/http-streamable-server branch April 7, 2025 18:52
@github-project-automation github-project-automation bot moved this from In Progress to Done in 2025-03-26 Implementation Apr 7, 2025
}));
return;
}
this.sessionId = this.sessionIdGenerator();
Copy link
Contributor

@beaulac beaulac Apr 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will throw if this.sessionIdGenerator is undefined - should it default to a () => undefined stub if it isn't passed in the constructor args?
(or could just change to this.sessionId = this.sessionIdGenerator?.();)

@calclavia
Copy link

Does this also contribute the Streamable HTTP client, or is it server only?

@dan-l
Copy link

dan-l commented Apr 8, 2025

Hi @ihrpr, in protocol.ts _onrequest, do we need to have these 3 transport send method pass along the related request id as well ?

@kentcdodds
Copy link

It appears this wasn't included in the latest release. Is there a release schedule I can look to so I can make plans around things? Also, it doesn't look like any doc updates were included in this pull request.

@ihrpr
Copy link
Contributor Author

ihrpr commented Apr 9, 2025

Also, it doesn't look like any doc updates were included in this pull request.

@kentcdodds I'll update docs once the the entire transport implementation is ready, including client and examples how to use and a few follow ups.

@kentcdodds
Copy link

Ah, I missed those PRs. Thanks @ihrpr!

Comment on lines +223 to +225
const hasRequests = messages.some(msg => 'method' in msg && 'id' in msg);
const hasOnlyNotificationsOrResponses = messages.every(msg =>
('method' in msg && !('id' in msg)) || ('result' in msg || 'error' in msg));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these ever both going to be false? (Maybe all errors?) If so we will fail to return a response at all unless I'm missing something.

If they are never both going to be false do you need both?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If they are never both going to be false do you need both?

Yes agreed, hasOnlyNotificationsOrResponses could probably be inlined into !hasRequests.
There should be different validation on the messages based on the spec since

The body of the POST request MUST be one of the following:
A single JSON-RPC request, notification, or response
An array batching one or more requests and/or notifications
An array batching one or more responses

is not correctly enforced as-is.


If so we will fail to return a response at all

Yes - this is to satisfy:

If the server accepts the input, the server MUST return HTTP status code 202 Accepted with no body.

(should actually probably be reworded to mention "accepts the input when there are no requests" or something, CTTOI)

and accords with JSON-RPC batch spec

A Response object SHOULD exist for each Request object
...
there SHOULD NOT be any Response objects for notifications
...
If there are no Response objects contained within the Response array as it is to be sent to the client, the server MUST NOT return an empty Array and should return nothing at all.

so res.writeHead(202).end(); below intentionally responds immediately with no body, the server can process the messages without blocking the ServerResponse.

This should be clarified with some additional code comments, imo.

Pizzaface pushed a commit to RewstApp/mcp-inspector that referenced this pull request May 2, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Development

Successfully merging this pull request may close these issues.

9 participants